/**
 * @addtogroup BAS
 * @{
 * @file
 * 日志操作
 */
#include <stdarg.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#include <sys/time.h>
#include <sys/stat.h>
#include "bas/bas_const.h"
#include "bas/bas_logger.h"

static const char mcaId[] = "$Id$";

/**
 * 默认日志,标准错误
 */
static T_Logger mtDefaultLogger = {LOGLEVEL_DEBUG, NULL, NULL, 0, false, ""};
/**
 * 默认日志指针
 */
static T_Logger *mptDefaultLogger = &mtDefaultLogger;

/**
 * @note 对于日志API内部的错误信息, 使用标准错误输出
 */
#define LOG_INTERNAL(...) basLoggerLog(&mtDefaultLogger, LOGLEVEL_ERROR, false, __FILE__, __LINE__, __func__, __VA_ARGS__)

//内部使用函数申明
static int _basLoggerChangeLog(T_Logger *ptLogger);

BAS_EXPORT void basLoggerLog(T_Logger *ptLogger,  E_LogLevel eLogLevel, const bool bIsPlainOutput,
                             const char *caFileName, const int iLineCount, const char *caFunctionName,
                             const char *caFormat, ...)
{
    T_Logger *ptLoggerInner = NULL;
    FILE *fp = NULL;
    struct timeval tTimeVal;
    va_list varList;
    char caTimeString[256];
    char caBuff[256];
    int iErrno = errno; //保存errno的值, 防止被后续操作修改

    //如果日志对象为空, 使用默认日志
    if (NULL == ptLogger)
    {
        ptLoggerInner = mptDefaultLogger;
    }
    else
    {
        ptLoggerInner = ptLogger;
    }

    //判断日志级别, 大于日志对象日志级别的日志不输出
    if (eLogLevel > ptLoggerInner->eLogLevel)
    {
        return;
    }

    //切换日志
    if (_basLoggerChangeLog(ptLoggerInner) != 0)
    {
        //切换日志失败, 将日志打印到标准错误
        ptLoggerInner->fpLogger = NULL;
    }

    fp = ptLoggerInner->fpLogger;
    if (NULL == fp)
    {
        fp = stderr;
    }

    if (!bIsPlainOutput)
    {
        fprintf(fp,"------------------------------------------------------------------------------\n");
        switch (eLogLevel)
        {
        case LOGLEVEL_INFO:
            fprintf(fp, "[INFO]");
            break;

        case LOGLEVEL_FATAL:
            fprintf(fp, "[FATAL]");
            break;

        case LOGLEVEL_ERROR:
            fprintf(fp, "[ERROR]");
            break;

        case LOGLEVEL_WARN:
            fprintf(fp, "[WARN]");
            break;

        case LOGLEVEL_DEBUG:
            fprintf(fp, "[DEBUG]");
            break;

        default:
            break;
        }

        //取日志时间, 精确到毫秒
        memset(caTimeString, 0, sizeof(caTimeString));
        memset(caBuff, 0, sizeof(caBuff));
        gettimeofday(&tTimeVal, NULL);
        strftime(caBuff, sizeof(caBuff), "TIME:%Y-%m-%d_%H:%M:%S",
                 localtime(&tTimeVal.tv_sec));
        snprintf(caTimeString, sizeof(caTimeString), "%s.%03ld",
                 caBuff, tTimeVal.tv_usec / 1000);

        //日志时间, 文件名, 行号
        fprintf(fp, "[%s][FILE:%s][LINE:%d]\n",
                caTimeString, 
                caFileName,
                iLineCount);

        //操作系统错误信息, INFO级别不输出
        if (iErrno != 0 && eLogLevel != LOGLEVEL_INFO)
        {
            fprintf(fp, "[OS:%s] ", strerror(iErrno)); 
            //重置errno 
            errno = 0;
        }
        //函数名
        fprintf(fp, "%s: ", caFunctionName);
    }

    //用户定义信息
    va_start(varList, caFormat);
    vfprintf(fp, caFormat, varList);
    fprintf(fp, "\n");
    va_end(varList);

    fflush(fp);
}

BAS_EXPORT T_Logger *basLoggerInit(const char *caFilePath, const size_t nChangeLogSize, const bool bIsDailyChange)
{
    T_Logger *ptLogger = NULL;
    char *caLogLevel = NULL;
    size_t nFilePathLength = 0;
    time_t tTime;

    if (NULL == (ptLogger = (T_Logger *)malloc(sizeof(T_Logger))))
    {
        LOG_INTERNAL("malloc failed");
        return NULL;
    }
    memset(ptLogger, 0, sizeof(T_Logger));

    //从环境变量LOGLEVEL取日志级别
    if (NULL == (caLogLevel = getenv("LOGLEVEL")))
    {
        basLoggerSetLogLevel(ptLogger, LOGLEVEL_DEBUG);
    }
    else
    {
        basLoggerSetLogLevel(ptLogger, atoi(caLogLevel));
    }
    
    //日志文件路径
    nFilePathLength = strlen(caFilePath);
    if (NULL == (ptLogger->caFilePath = (char *)malloc(nFilePathLength + 1)))
    {
        LOG_INTERNAL("malloc failed");
        return NULL;
    }
    strncpy(ptLogger->caFilePath, caFilePath, nFilePathLength); 
    ptLogger->caFilePath[nFilePathLength] = '\0';

    //日志文件指针
    if (NULL == (ptLogger->fpLogger = fopen(ptLogger->caFilePath, "a+")))
    {
        LOG_INTERNAL("fopen failed: file path[%s]", ptLogger->caFilePath); 
        return NULL;
    }

    //日志文件切换的文件大小
    ptLogger->nChangeLogSize = nChangeLogSize;

    //是否要日切换
    ptLogger->bIsDailyChange = bIsDailyChange;

    //上次切换日志的日期
    time(&tTime);
    strftime(ptLogger->caLastChangeDate, sizeof(ptLogger->caLastChangeDate),
             "%Y%m%d", localtime(&tTime));

    return ptLogger;
}

BAS_EXPORT E_LogLevel basLoggerGetLogLevel(T_Logger *ptLogger)
{
    return ptLogger->eLogLevel;
}

BAS_EXPORT E_LogLevel basLoggerSetLogLevel(T_Logger *ptLogger, const E_LogLevel eLogLevel)
{
    E_LogLevel iOldLogLevel = ptLogger->eLogLevel; 

    if (eLogLevel < LOGLEVEL_INFO)
    {
        ptLogger->eLogLevel = LOGLEVEL_INFO;
    }
    else if (eLogLevel > LOGLEVEL_DEBUG)
    {
        ptLogger->eLogLevel = LOGLEVEL_DEBUG;
    }
    else
    {
        ptLogger->eLogLevel = eLogLevel;
    }

    return iOldLogLevel;
}

BAS_EXPORT void basLoggerClose(T_Logger *ptLogger)
{
    if (NULL == ptLogger)
    {
        return;
    }

    //如果用户企图关闭内置默认日志, 直接返回
    if (&mtDefaultLogger == ptLogger)
    {
        return;
    }

    //关闭文件指针
    if (ptLogger->fpLogger != NULL)
    {
        fclose(ptLogger->fpLogger);
    }

    //释放内存
    if (ptLogger->caFilePath != NULL)
    {
        free(ptLogger->caFilePath);
    }
    free(ptLogger);
}

BAS_EXPORT T_Logger *basLoggerGetDefault(void)
{
    return mptDefaultLogger;
}

BAS_EXPORT T_Logger *basLoggerSetDefault(T_Logger *ptLogger)
{
    T_Logger *ptOldLogger = NULL; 

    if (NULL == ptLogger)
    {
        return mptDefaultLogger;
    }

    ptOldLogger = mptDefaultLogger;
    mptDefaultLogger = ptLogger;

    return ptOldLogger;
}

BAS_EXPORT FILE *basLoggerGetFileHandle(T_Logger *ptLogger)
{
    if (NULL == ptLogger)
    {
        return NULL;
    }

    return ptLogger->fpLogger;
}

/**
 * @brief         切换日志
 * @param[in,out] ptLogger 日志对象指针
 * @returns       0 - 成功 -1 - 失败
 * @note          本函数为内部使用函数
 * @see           basLoggerInit
 */
static int _basLoggerChangeLog(T_Logger *ptLogger)
{
    struct stat tFileStat; 
    char *caRenameFilePath = NULL;
    size_t nRenameFilePathLength = 0;
    struct timeval tTimeVal;
    char caTimeString[256];
    char caDate[8 + 1];

    //标准错误不需要切换日志
    if (NULL == ptLogger->fpLogger)
    {
        return 0;
    }

    //取文件信息, 此处不能使用fstat, 否则日志文件被删除检测不到
    if (stat(ptLogger->caFilePath, &tFileStat) != 0)
    {
        //如果日志文件被删除, 尝试重新新建文件
        if (ENOENT == errno)
        {
            fclose(ptLogger->fpLogger);
            if (NULL == (ptLogger->fpLogger = fopen(ptLogger->caFilePath, "a+")))
            {
                LOG_INTERNAL("fopen failed: file path[%s]", ptLogger->caFilePath); 
                return -1;
            }

            //新建文件后不需要切换日志, 直接返回
            return 0;
        }
        else
        {
            LOG_INTERNAL("stat failed");
            return -1;
        }
    }

    do
    {
        //切换文件大小不为0, 且文件大小达到nChangeLogSize(MB)时, 需要切换
        if (ptLogger->nChangeLogSize != 0
            && tFileStat.st_size >= ptLogger->nChangeLogSize * 1024 * 1024)
        {
            break; 
        }

        //需要日切换, 且日期变换时, 需要切换
        if (ptLogger->bIsDailyChange)
        {
            gettimeofday(&tTimeVal, NULL);
            strftime(caDate, sizeof(caDate), "%Y%m%d", localtime(&tTimeVal.tv_sec));
            if (strncmp(caDate, ptLogger->caLastChangeDate, sizeof(caDate)) != 0)
            {
                //赋上新日期
                strncpy(ptLogger->caLastChangeDate, caDate,
                        sizeof(ptLogger->caLastChangeDate) - 1);
                break;
            }
        }

        return 0;
    } while (false);
    
    //切换日志
    nRenameFilePathLength = strlen(ptLogger->caFilePath) + 
                            strlen(".YY_MM_DD_HH_MM_SS_XXX");
    if (NULL == (caRenameFilePath = (char *)malloc(nRenameFilePathLength + 1)))
    {
        LOG_INTERNAL("malloc failed");
        return -1;
    }

    //---取重命名后的文件路径
    gettimeofday(&tTimeVal, NULL);
    strftime(caTimeString, sizeof(caTimeString), "%y_%m_%d_%H_%M_%S",
             localtime(&tTimeVal.tv_sec));
    snprintf(caRenameFilePath, nRenameFilePathLength + 1,
             "%s.%s_%03ld", ptLogger->caFilePath, caTimeString, tTimeVal.tv_usec / 1000);

    //---关闭文件指针
    fclose(ptLogger->fpLogger);

    //---重命名文件
    if (rename(ptLogger->caFilePath, caRenameFilePath) != 0)
    {
        LOG_INTERNAL("rename failed");
        return -1;
    }

    //---打开新文件
    if (NULL == (ptLogger->fpLogger = fopen(ptLogger->caFilePath, "a+")))
    {
        LOG_INTERNAL("fopen failed: file path[%s]", ptLogger->caFilePath); 
        return -1;
    }

    return 0;
}

/** @} */
